/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.search;

import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.search.Query;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.search.function.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class FunctionQParser extends QParser {

	/** @lucene.internal */
	public QueryParsing.StrParser sp;
	boolean parseMultipleSources = true;
	boolean parseToEnd = true;

	public FunctionQParser(String qstr, SolrParams localParams,
			SolrParams params, SolrQueryRequest req) {
		super(qstr, localParams, params, req);
	}

	public void setParseMultipleSources(boolean parseMultipleSources) {
		this.parseMultipleSources = parseMultipleSources;
	}

	/** parse multiple comma separated value sources */
	public boolean getParseMultipleSources() {
		return parseMultipleSources;
	}

	public void setParseToEnd(boolean parseToEnd) {
		this.parseToEnd = parseToEnd;
	}

	/**
	 * throw exception if there is extra stuff at the end of the parsed
	 * valuesource(s).
	 */
	public boolean getParseToEnd() {
		return parseMultipleSources;
	}

	@Override
	public Query parse() throws ParseException {
		sp = new QueryParsing.StrParser(getString());

		ValueSource vs = null;
		List<ValueSource> lst = null;

		for (;;) {
			ValueSource valsource = parseValueSource(false);
			sp.eatws();
			if (!parseMultipleSources) {
				vs = valsource;
				break;
			} else {
				if (lst != null) {
					lst.add(valsource);
				} else {
					vs = valsource;
				}
			}

			// check if there is a "," separator
			if (sp.peek() != ',')
				break;

			consumeArgumentDelimiter();

			if (lst == null) {
				lst = new ArrayList<ValueSource>(2);
				lst.add(valsource);
			}
		}

		if (parseToEnd && sp.pos < sp.end) {
			throw new ParseException("Unexpected text after function: "
					+ sp.val.substring(sp.pos, sp.end));
		}

		if (lst != null) {
			vs = new VectorValueSource(lst);
		}

		return new FunctionQuery(vs);
	}

	/**
	 * Are there more arguments in the argument list being parsed?
	 * 
	 * @return whether more args exist
	 * @throws ParseException
	 */
	public boolean hasMoreArguments() throws ParseException {
		int ch = sp.peek();
		/* determine whether the function is ending with a paren or end of str */
		return (!(ch == 0 || ch == ')'));
	}

	/**
	 * TODO: Doc
	 * 
	 * @throws ParseException
	 */
	public String parseId() throws ParseException {
		String value = parseArg();
		if (argWasQuoted)
			throw new ParseException(
					"Expected identifier instead of quoted string:" + value);
		return value;
	}

	/**
	 * Parse a float.
	 * 
	 * @return Float
	 * @throws ParseException
	 */
	public Float parseFloat() throws ParseException {
		String str = parseArg();
		if (argWasQuoted())
			throw new ParseException("Expected float instead of quoted string:"
					+ str);
		float value = Float.parseFloat(str);
		return value;
	}

	/**
	 * Parse a Double
	 * 
	 * @return double
	 * @throws ParseException
	 */
	public double parseDouble() throws ParseException {
		String str = parseArg();
		if (argWasQuoted())
			throw new ParseException(
					"Expected double instead of quoted string:" + str);
		double value = Double.parseDouble(str);
		return value;
	}

	/**
	 * Parse an integer
	 * 
	 * @return An int
	 * @throws ParseException
	 */
	public int parseInt() throws ParseException {
		String str = parseArg();
		if (argWasQuoted())
			throw new ParseException(
					"Expected double instead of quoted string:" + str);
		int value = Integer.parseInt(str);
		return value;
	}

	private boolean argWasQuoted;

	public boolean argWasQuoted() {
		return argWasQuoted;
	}

	public String parseArg() throws ParseException {
		argWasQuoted = false;

		sp.eatws();
		char ch = sp.peek();
		String val = null;
		switch (ch) {
		case ')':
			return null;
		case '$':
			sp.pos++;
			String param = sp.getId();
			val = getParam(param);
			break;
		case '\'':
		case '"':
			val = sp.getQuotedString();
			argWasQuoted = true;
			break;
		default:
			// read unquoted literal ended by whitespace ',' or ')'
			// there is no escaping.
			int valStart = sp.pos;
			for (;;) {
				if (sp.pos >= sp.end) {
					throw new ParseException(
							"Missing end to unquoted value starting at "
									+ valStart + " str='" + sp.val + "'");
				}
				char c = sp.val.charAt(sp.pos);
				if (c == ')' || c == ',' || Character.isWhitespace(c)) {
					val = sp.val.substring(valStart, sp.pos);
					break;
				}
				sp.pos++;
			}
		}

		sp.eatws();
		consumeArgumentDelimiter();
		return val;
	}

	/**
	 * Parse a list of ValueSource. Must be the final set of arguments to a
	 * ValueSource.
	 * 
	 * @return List<ValueSource>
	 * @throws ParseException
	 */
	public List<ValueSource> parseValueSourceList() throws ParseException {
		List<ValueSource> sources = new ArrayList<ValueSource>(3);
		while (hasMoreArguments()) {
			sources.add(parseValueSource(true));
		}
		return sources;
	}

	/**
	 * Parse an individual ValueSource.
	 * 
	 * @throws ParseException
	 */
	public ValueSource parseValueSource() throws ParseException {
		/*
		 * consume the delimiter afterward for an external call to
		 * parseValueSource
		 */
		return parseValueSource(true);
	}

	/**
	 * TODO: Doc
	 * 
	 * @throws ParseException
	 */
	public Query parseNestedQuery() throws ParseException {
		Query nestedQuery;

		if (sp.opt("$")) {
			String param = sp.getId();
			String qstr = getParam(param);
			qstr = qstr == null ? "" : qstr;
			nestedQuery = subQuery(qstr, null).getQuery();
		} else {
			int start = sp.pos;
			String v = sp.val;

			String qs = v;
			HashMap nestedLocalParams = new HashMap<String, String>();
			int end = QueryParsing.parseLocalParams(qs, start,
					nestedLocalParams, getParams());

			QParser sub;

			if (end > start) {
				if (nestedLocalParams.get(QueryParsing.V) != null) {
					// value specified directly in local params... so the end of
					// the
					// query should be the end of the local params.
					sub = subQuery(qs.substring(start, end), null);
				} else {
					// value here is *after* the local params... ask the parser.
					sub = subQuery(qs, null);
					// int subEnd = sub.findEnd(')');
					// TODO.. implement functions to find the end of a nested
					// query
					throw new ParseException(
							"Nested local params must have value in v parameter.  got '"
									+ qs + "'");
				}
			} else {
				throw new ParseException(
						"Nested function query must use $param or {!v=value} forms. got '"
								+ qs + "'");
			}

			sp.pos += end - start; // advance past nested query
			nestedQuery = sub.getQuery();
		}
		consumeArgumentDelimiter();

		return nestedQuery;
	}

	/**
	 * Parse an individual value source.
	 * 
	 * @param doConsumeDelimiter
	 *            whether to consume a delimiter following the ValueSource
	 * @throws ParseException
	 */
	protected ValueSource parseValueSource(boolean doConsumeDelimiter)
			throws ParseException {
		ValueSource valueSource;

		int ch = sp.peek();
		if (ch >= '0' && ch <= '9' || ch == '.' || ch == '+' || ch == '-') {
			Number num = sp.getNumber();
			if (num instanceof Long) {
				valueSource = new LongConstValueSource(num.longValue());
			} else if (num instanceof Double) {
				valueSource = new DoubleConstValueSource(num.doubleValue());
			} else {
				// shouldn't happen
				valueSource = new ConstValueSource(num.floatValue());
			}
		} else if (ch == '"' || ch == '\'') {
			valueSource = new LiteralValueSource(sp.getQuotedString());
		} else if (ch == '$') {
			sp.pos++;
			String param = sp.getId();
			String val = getParam(param);
			if (val == null) {
				throw new ParseException("Missing param " + param
						+ " while parsing function '" + sp.val + "'");
			}

			QParser subParser = subQuery(val, "func");
			if (subParser instanceof FunctionQParser) {
				((FunctionQParser) subParser).setParseMultipleSources(true);
			}
			Query subQuery = subParser.getQuery();
			if (subQuery instanceof FunctionQuery) {
				valueSource = ((FunctionQuery) subQuery).getValueSource();
			} else {
				valueSource = new QueryValueSource(subQuery, 0.0f);
			}

			/***
			 * // dereference *simple* argument (i.e., can't currently be a
			 * function) // In the future we could support full function
			 * dereferencing via a stack of ValueSource (or StringParser)
			 * objects ch = val.length()==0 ? '\0' : val.charAt(0);
			 * 
			 * if (ch>='0' && ch<='9' || ch=='.' || ch=='+' || ch=='-') {
			 * QueryParsing.StrParser sp = new QueryParsing.StrParser(val);
			 * Number num = sp.getNumber(); if (num instanceof Long) {
			 * valueSource = new LongConstValueSource(num.longValue()); } else
			 * if (num instanceof Double) { valueSource = new
			 * DoubleConstValueSource(num.doubleValue()); } else { // shouldn't
			 * happen valueSource = new ConstValueSource(num.floatValue()); } }
			 * else if (ch == '"' || ch == '\'') { QueryParsing.StrParser sp =
			 * new QueryParsing.StrParser(val); val = sp.getQuotedString();
			 * valueSource = new LiteralValueSource(val); } else { if
			 * (val.length()==0) { valueSource = new LiteralValueSource(val); }
			 * else { String id = val; SchemaField f =
			 * req.getSchema().getField(id); valueSource =
			 * f.getType().getValueSource(f, this); } }
			 ***/

		} else {

			String id = sp.getId();
			if (sp.opt("(")) {
				// a function... look it up.
				ValueSourceParser argParser = req.getCore()
						.getValueSourceParser(id);
				if (argParser == null) {
					throw new ParseException("Unknown function " + id
							+ " in FunctionQuery(" + sp + ")");
				}
				valueSource = argParser.parse(this);
				sp.expect(")");
			} else {
				SchemaField f = req.getSchema().getField(id);
				valueSource = f.getType().getValueSource(f, this);
			}

		}

		if (doConsumeDelimiter)
			consumeArgumentDelimiter();

		return valueSource;
	}

	/**
	 * Consume an argument delimiter (a comma) from the token stream. Only
	 * consumes if more arguments should exist (no ending parens or end of
	 * string).
	 * 
	 * @return whether a delimiter was consumed
	 * @throws ParseException
	 */
	protected boolean consumeArgumentDelimiter() throws ParseException {
		/* if a list of args is ending, don't expect the comma */
		if (hasMoreArguments()) {
			sp.expect(",");
			return true;
		}

		return false;
	}

}
